Skip to content

Add HTTP transport and Bearer token auth for Google Cloud Run deployment - #11

Open
hemati wants to merge 5 commits into
Arindam200:mainfrom
hemati:main
Open

Add HTTP transport and Bearer token auth for Google Cloud Run deployment#11
hemati wants to merge 5 commits into
Arindam200:mainfrom
hemati:main

Conversation

@hemati

@hemati hemati commented Dec 24, 2025

Copy link
Copy Markdown
Contributor

This pull request adds support for deploying the Reddit MCP server to Google Cloud Run using HTTP transport, including optional Bearer Token authentication for secure access. The changes also improve local development and testing workflows, update dependencies to support HTTP serving, and enhance error handling and logging throughout the codebase.

Deployment and Authentication Enhancements:

  • Added detailed instructions to README.md for deploying to Google Cloud Run, including environment variable configuration, authentication setup, and log monitoring.
  • Introduced Bearer Token authentication middleware for HTTP endpoints, activated via the MCP_BEARER_TOKEN environment variable and implemented using Starlette middleware.
  • Updated the __main__ section in server.py to support both stdio and HTTP transports, with conditional middleware injection for authentication and Uvicorn server startup for HTTP mode.
  • Added a Procfile to specify the web server startup command for deployment platforms.

Dependency and Configuration Updates:

  • Updated pyproject.toml to add fastmcp, uvicorn, and an optional auth dependency group for Starlette-based authentication.
  • Ensured praw.models is explicitly imported for type checking and improved type annotations for comment formatting. [1] [2]

Robustness and Logging Improvements:

  • Improved error handling when replying to posts and comments by raising an error if Reddit returns no response. [1] [2]
  • Fixed a potential unbound variable error when extracting submission IDs.

Summary by CodeRabbit

  • New Features

    • Added authenticated messaging with recipient verification, content limits, and clear handling of messaging restrictions.
    • Added configurable HTTP and stdio service modes with optional Bearer token authentication.
    • Improved compatibility when displaying account and community information.
  • Documentation

    • Added Google Cloud Run deployment, configuration, testing, monitoring, and troubleshooting guidance.
  • Chores

    • Added web deployment configuration and environment file exclusions.
    • Updated and consolidated project dependencies.

@coderabbitai

coderabbitai Bot commented Dec 24, 2025

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds authenticated Reddit messaging, configurable FastMCP HTTP or stdio execution, optional Bearer-token middleware, deployment dependencies, Cloud Run documentation, and runtime safeguards for Reddit responses.

Changes

Reddit MCP runtime and deployment

Layer / File(s) Summary
Authenticated messaging and Reddit model handling
server.py, README.md
Adds send_message with input validation, recipient verification, self-message warnings, delivery metadata, and mapped Reddit errors. Extends comment formatting and normalizes newer subreddit objects.
Configurable HTTP transport and Bearer authentication
server.py, README.md
Configures FastMCP host, port, and stateless HTTP settings from environment variables. Adds optional Starlette Bearer-token validation and selects HTTP or stdio startup. Documents Cloud Run deployment and local transport testing.
Runtime safeguards and access documentation
server.py
Handles missing Reddit reply objects, initializes submission identifiers before error handling, and documents singleton and write-access wrappers.
Deployment dependencies and local environment support
Procfile, pyproject.toml, requirements.txt, .gitignore
Adds the web process command, updates dependency minimums, adds authentication extras, and ignores local virtual environments.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant MCPClient
  participant Uvicorn
  participant BearerTokenMiddleware
  participant FastMCP
  participant RedditAPI

  MCPClient->>Uvicorn: Send HTTP MCP request
  Uvicorn->>BearerTokenMiddleware: Forward request
  BearerTokenMiddleware->>BearerTokenMiddleware: Validate Bearer token
  BearerTokenMiddleware->>FastMCP: Dispatch authorized request
  FastMCP->>RedditAPI: Verify recipient or perform Reddit operation
  RedditAPI-->>FastMCP: Return result or mapped error
  FastMCP-->>MCPClient: Return MCP response
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: HTTP transport and Bearer token authentication for Google Cloud Run deployment.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
server.py (1)

155-208: Consider constant-time comparison for Bearer token validation.

The Bearer token authentication middleware is well-structured with proper error responses (401 for missing/invalid format, 403 for wrong token). However, the token comparison at line 192 uses standard string comparison, which may be vulnerable to timing attacks.

🔎 Proposed fix using secrets.compare_digest

Add import at the top of the file:

import secrets

Then update the token comparison:

                 # Extract and validate token
                 token = auth_header[7:]  # Remove "Bearer " prefix
-                if token != self.bearer_token:
+                if not secrets.compare_digest(token, self.bearer_token):
                     return JSONResponse(
                         status_code=403,
                         content={"error": "Invalid bearer token"}
                     )

secrets.compare_digest performs constant-time comparison, making timing attacks significantly harder.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c56363 and afe37a2.

📒 Files selected for processing (5)
  • Procfile
  • README.md
  • pyproject.toml
  • requirements.txt
  • server.py
🧰 Additional context used
🪛 Gitleaks (8.30.0)
README.md

[high] 291-296: Discovered a potential authorization token provided in a curl command header, which could compromise the curl accessed resource.

(curl-auth-header)

🪛 LanguageTool
README.md

[grammar] ~262-~262: Ensure spelling is correct
Context: ...} } } } ``` ### Local Testing **Stdio mode (default, for local MCP clients):*...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 Ruff (0.14.10)
server.py

140-140: Possible binding to all interfaces

(S104)


1432-1432: Abstract raise to an inner function

(TRY301)


1432-1432: Avoid specifying long messages outside the exception class

(TRY003)


1537-1537: Abstract raise to an inner function

(TRY301)


1537-1537: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (9)
README.md (1)

173-342: Well-documented Cloud Run deployment guide.

The deployment instructions are comprehensive and cover:

  • Prerequisites and authentication setup
  • Basic and secure (Bearer token) deployment options
  • MCP client configuration examples
  • Local testing for both stdio and HTTP modes
  • Monitoring and operational commands

The documentation structure is clear and provides actionable commands.

Procfile (1)

1-1: LGTM! Simple and correct.

The Procfile correctly defines the web process for Heroku-style deployments. The server.py main block will handle transport selection based on the MCP_TRANSPORT environment variable.

server.py (7)

9-9: Good addition for type checking.

Explicitly importing praw.models improves type annotations and IDE support, especially for the updated _format_comment signature.


138-152: Environment-driven configuration is well-designed.

The FastMCP configuration correctly reads from environment variables:

  • HOST defaults to 0.0.0.0 (appropriate for Cloud Run containers)
  • PORT defaults to 8080 (Cloud Run standard)
  • MCP_STATELESS enables stateless HTTP mode for serverless deployments

The static analysis warning about binding to all interfaces (0.0.0.0) is a false positive here—Cloud Run containers require this to accept traffic.


332-332: Improved type annotation for flexibility.

Updating the type hint to accept both praw.models.Comment and praw.models.Message improves type safety and correctly reflects that the function can handle both types of objects.


1431-1432: Good defensive check for API response.

Adding validation that reply is not None prevents potential issues if the Reddit API fails silently. The explicit error message helps with debugging.


1536-1537: Good defensive check for API response.

Consistent with the pattern in reply_to_post, this defensive check ensures the Reddit API returned a valid response.


1906-1908: Good fix for potential UnboundLocalError.

Initializing clean_submission_id before the try block ensures it's defined if _extract_reddit_id raises an exception, preventing an UnboundLocalError when the exception handler references it at line 2038.


2402-2434: Well-structured entry point with proper transport selection.

The __main__ block correctly handles three modes:

  1. HTTP with Bearer token authentication (manual uvicorn with middleware)
  2. HTTP without authentication (standard mcp.run)
  3. Stdio mode (backward-compatible default)

The conditional logic is clear, and logging messages help with debugging deployment issues.

Comment thread pyproject.toml Outdated
Comment thread requirements.txt Outdated
…erver.py with detailed docstrings for better clarity
@hemati hemati changed the title Added Google Cloud Run Deploy Instructions Add HTTP transport and Bearer token auth for Google Cloud Run deployment Dec 24, 2025

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
server.py (1)

184-231: Consider using constant-time comparison for bearer token validation.

The bearer token comparison at line 225 uses a standard equality check, which may be vulnerable to timing attacks. For security-sensitive token validation, consider using a constant-time comparison function.

🔎 Proposed fix using secrets.compare_digest

Add import at the top of the file:

+import secrets
 import functools
 import logging

Then update the token comparison:

                 # Extract and validate token
                 token = auth_header[7:]  # Remove "Bearer " prefix
-                if token != self.bearer_token:
+                if not secrets.compare_digest(token, self.bearer_token):
                     return JSONResponse(
                         status_code=403,
                         content={"error": "Invalid bearer token"}
                     )
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between afe37a2 and 0fe003e.

📒 Files selected for processing (3)
  • pyproject.toml
  • requirements.txt
  • server.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • requirements.txt
🧰 Additional context used
🪛 Ruff (0.14.10)
server.py

158-158: Possible binding to all interfaces

(S104)


1465-1465: Abstract raise to an inner function

(TRY301)


1465-1465: Avoid specifying long messages outside the exception class

(TRY003)


1570-1570: Abstract raise to an inner function

(TRY301)


1570-1570: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (7)
server.py (7)

9-9: LGTM!

Explicit import of praw.models improves type checking and makes type annotations clearer.


28-32: LGTM!

The docstring additions improve code documentation and follow Python best practices.

Also applies to: 128-140


156-170: LGTM! Configuration appropriate for Cloud Run deployment.

The environment-based configuration is well-structured. Binding to 0.0.0.0 (flagged by static analysis) is intentional and necessary for Cloud Run deployments to accept external traffic. The stateless HTTP mode is appropriate for serverless environments.


365-365: LGTM!

The type annotation improvement makes the function's accepted types explicit and enhances type safety.


1464-1465: LGTM! Good defensive programming.

The checks for None replies add robustness by ensuring Reddit API calls succeeded before proceeding. This prevents silent failures and provides clear error messages.

Also applies to: 1569-1570


1939-1941: LGTM! Good fix for potential UnboundLocalError.

Initializing clean_submission_id before the try block ensures the variable is always bound when referenced in the exception handler, preventing potential errors.


2436-2467: LGTM! Well-structured transport selection and server startup.

The implementation properly handles both HTTP and stdio transports, with backward compatibility maintained through the stdio default. The conditional middleware injection for Bearer token authentication is correctly implemented, and logging provides good visibility into the server's configuration.

Comment thread pyproject.toml Outdated
@hemati

hemati commented Dec 24, 2025

Copy link
Copy Markdown
Contributor Author

@Arindam200 this is a larger PR, but it adds an important new feature: using the MCP over HTTP.
I also included a short guide on how to deploy it on Google Cloud.

who_am_i was broken on praw >= 8: current_user.subreddit now returns a
UserSubreddit object rather than a dict, so the nine .get() calls below it
failed with "'UserSubreddit' object has no attribute 'get'". The object is
now converted to a dict first; the surrounding code is unchanged.

send_message(username, subject, message) is new, modelled on
reply_to_comment and gated by @require_write_access. It resolves the
recipient before sending so a typo fails locally instead of silently at
Reddit, enforces Reddit's 100 character subject limit and 1..10000 for the
body, accepts u/name, /u/name and name alike, and translates rate limits,
blocked recipients and unknown users into readable errors. Reddit returns no
message id on success, which the result notes.

Delivery itself is untested: Reddit refuses messages to the authenticated
account itself (NOT_WHITELISTED_BY_USER_MESSAGE), and any other recipient
would have been a real person. Everything up to and including the API call
is verified, as is every validation path.

Also ignore venv/ and env/. Only .venv was listed, so a `git add -A` in a
checkout using venv/ would have committed the whole virtual environment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011vLKdeiPdvNQYxJkXw5dYx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 107-111: Update the send_message documentation to state that
self-messages trigger a warning and are still sent, matching server.py behavior;
alternatively, change send_message to reject self-messages and preserve the
existing refusal statement.

In `@requirements.txt`:
- Around line 1-4: Add starlette>=0.49.1 to the dependency list and update
server.py startup so configuring MCP_BEARER_TOKEN without Starlette raises an
error instead of clearing bearer_token or starting unauthenticated; preserve
normal startup when authentication is not configured.

In `@server.py`:
- Around line 1647-1653: Update the recipient lookup exception handling around
redditor.id to catch only prawcore.exceptions.NotFound and raise the existing
nonexistent-user ValueError from it; allow all other lookup exceptions to
propagate to the existing outer handler so they become RuntimeError.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b09fa70a-a78c-4561-a4fc-c6f388c0bfd2

📥 Commits

Reviewing files that changed from the base of the PR and between 0fe003e and 84b9da6.

📒 Files selected for processing (5)
  • .gitignore
  • README.md
  • pyproject.toml
  • requirements.txt
  • server.py

Comment thread README.md
Comment on lines +107 to +111
- `send_message(username, subject, message)` - Send a private message to a user.
Subject is capped at 100 characters by Reddit, body at 10000. The recipient is
verified before sending, so a typo fails locally instead of silently at Reddit.
Note that Reddit returns no message id on success and refuses messages to the
authenticated account itself.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the self-message behavior statement.

The README says that the tool refuses messages to the authenticated account. server.py only logs a warning and then sends the message. Change this text to state that the tool warns, or reject self-messages in send_message.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 107 - 111, Update the send_message documentation to
state that self-messages trigger a warning and are still sent, matching
server.py behavior; alternatively, change send_message to reject self-messages
and preserve the existing refusal statement.

Comment thread requirements.txt
Comment on lines +1 to +4
mcp[cli]>=1.23.0
praw>=7.8.1
uvicorn>=0.40.0
fastmcp>=2.14.1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the dependency installation path used by deployment.
rg -n -C 3 \
  'requirements\.txt|pip(3)? install|poetry install|uv sync|\.\[auth\]|fastmcp|starlette|MCP_BEARER_TOKEN' \
  -g 'Procfile' \
  -g 'Dockerfile*' \
  -g '*.yml' \
  -g '*.yaml' \
  -g 'README.md' \
  -g 'pyproject.toml' \
  -g 'requirements.txt' \
  -g 'server.py' . || true

Repository: Arindam200/reddit-mcp

Length of output: 4088


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- dependency manifests ---'
cat -n requirements.txt
sed -n '1,35p' pyproject.toml

printf '%s\n' '--- deployment and auth instructions ---'
sed -n '215,245p' README.md
sed -n '275,305p' README.md

printf '%s\n' '--- server authentication branch ---'
sed -n '1,25p' server.py
sed -n '165,245p' server.py

printf '%s\n' '--- relevant references ---'
rg -n -C 3 'requirements\.txt|pyproject\.toml|MCP_BEARER_TOKEN|starlette|fastmcp' \
  Dockerfile Procfile README.md pyproject.toml requirements.txt server.py 2>/dev/null || true

Repository: Arindam200/reddit-mcp

Length of output: 9422


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- middleware usage and startup path ---'
rg -n -C 6 'BearerTokenMiddleware|middleware|run\(|MCP_TRANSPORT|streamable-http|http_app|app' server.py

printf '%s\n' '--- server structure ---'
ast-grep outline server.py --view compact

Repository: Arindam200/reddit-mcp

Length of output: 13107


Make bearer-token authentication fail closed.

When MCP_BEARER_TOKEN is set but Starlette is unavailable, server.py clears bearer_token. HTTP startup then runs without authentication middleware. Make startup fail and add starlette>=0.49.1 to requirements.txt or deploy with .[auth].

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@requirements.txt` around lines 1 - 4, Add starlette>=0.49.1 to the dependency
list and update server.py startup so configuring MCP_BEARER_TOKEN without
Starlette raises an error instead of clearing bearer_token or starting
unauthenticated; preserve normal startup when authentication is not configured.

Comment thread server.py
Comment on lines +1647 to +1653
try:
recipient_id = redditor.id
except Exception as lookup_error:
logger.exception(f"Recipient u/{clean_username} not found: {lookup_error}")
raise ValueError(
f"User u/{clean_username} does not exist or is not reachable"
) from lookup_error

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For PRAW version 7.8.1, which exception classes can reddit.redditor(name).id raise for a nonexistent account, HTTP request failure, and forbidden access? Use official PRAW or prawcore documentation and source.

💡 Result:

In PRAW version 7.8.1, accessing the id attribute of a lazy Redditor object triggers a network request, which can raise specific exceptions from the prawcore library when the request fails or encounters an error [1][2]. These exceptions are part of prawcore, the low-level communication layer used by PRAW [3][4]. The following prawcore.exceptions classes are raised in these scenarios: 1. Nonexistent Account: When attempting to access the id of a nonexistent user, PRAW raises prawcore.exceptions.NotFound [5][2]. This indicates a 404 HTTP response from Reddit's API [6][4]. 2. Forbidden Access: When access is forbidden (e.g., attempting to access data that the current user does not have permission to view, or interacting with a restricted/banned account), PRAW raises prawcore.exceptions.Forbidden [7][8]. This corresponds to a 403 HTTP response [7][6]. 3. HTTP Request Failure: General network or HTTP-level failures are typically raised as specific subclasses of prawcore.PrawcoreException [9][10]. Common exceptions include: * prawcore.exceptions.RequestException: Raised for errors occurring during the attempt to make the HTTP request (e.g., connection errors) [9][6]. * prawcore.exceptions.ServerError: Raised when a 5xx HTTP response is received from the server [9][6]. * prawcore.exceptions.TooManyRequests: Raised when hitting rate limits (429 HTTP status) [6][4]. To handle these, you should import the exceptions directly from prawcore.exceptions [5][7]. PRAW itself does not wrap all prawcore exceptions, so catching them directly is the standard practice [7]. Example usage: from prawcore.exceptions import NotFound, Forbidden, RequestException try: user_id = reddit.redditor("nonexistent_user").id except NotFound: print("User does not exist.") except Forbidden: print("Access is forbidden.") except RequestException: print("A network or HTTP request error occurred.")

Citations:


🏁 Script executed:

sed -n '1,120p' server.py
sed -n '1580,1720p' server.py
rg -n "prawcore|NotFound|Forbidden|RequestException|ServerError|TooManyRequests|RuntimeError|does not exist or is not reachable" server.py pyproject.toml requirements*.txt uv.lock 2>/dev/null

Repository: Arindam200/reddit-mcp

Length of output: 16215


🏁 Script executed:

printf '%s\n' '--- imports ---'
sed -n '1,120p' server.py
printf '%s\n' '--- send_message ---'
sed -n '1580,1720p' server.py
printf '%s\n' '--- related symbols ---'
rg -n "prawcore|NotFound|Forbidden|RequestException|ServerError|TooManyRequests|RuntimeError|does not exist or is not reachable" server.py pyproject.toml requirements*.txt uv.lock 2>/dev/null || true

Repository: Arindam200/reddit-mcp

Length of output: 16276


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
import re
text = Path("uv.lock").read_text()
for name in ("praw", "prawcore"):
    match = re.search(rf'(?ms)^name = "{name}"\nversion = "([^"]+)"', text)
    print(f"{name}={match.group(1) if match else 'not found'}")
PY

python3 - <<'PY'
from urllib.request import urlopen
urls = [
    "https://raw.githubusercontent.com/praw-dev/praw/v7.8.1/praw/models/redditor.py",
    "https://raw.githubusercontent.com/praw-dev/prawcore/v2.4.0/prawcore/exceptions.py",
]
for url in urls:
    print(f"--- {url} ---")
    try:
        body = urlopen(url, timeout=10).read().decode()
        for i, line in enumerate(body.splitlines(), 1):
            if "class Redditor" in line or "def id" in line or "class NotFound" in line or "class Forbidden" in line or "class RequestException" in line:
                print(f"{i}: {line}")
    except Exception as exc:
        print(type(exc).__name__, exc)
PY

Repository: Arindam200/reddit-mcp

Length of output: 650


Handle recipient lookup exceptions by type.

Catch prawcore.exceptions.NotFound and raise the nonexistent-user ValueError. Let other lookup exceptions reach the existing outer handler, which converts them to RuntimeError.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server.py` around lines 1647 - 1653, Update the recipient lookup exception
handling around redditor.id to catch only prawcore.exceptions.NotFound and raise
the existing nonexistent-user ValueError from it; allow all other lookup
exceptions to propagate to the existing outer handler so they become
RuntimeError.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants